/**
    @file pixelplayer.cpp
    @brief Player
*/
#include "pixel/pixelplayer.hpp"
#include "lua/lua_pixelplayer.hpp"

pm3::PIXELPLAYER *pm3::g_player = NULL;

pm3::PIXELPLAYER::PIXELPLAYER() {
    m_camera_node = NULL;
    m_camera_yaw_node = NULL;
    m_camera_pitch_node = NULL;
    m_camera_roll_node = NULL;
    m_camera = NULL;

    m_state = NULL;
    m_rigid_body = NULL;
    m_collision_shape = NULL;

    m_god_mode = false;

    m_physics_active = true;

    m_mouse_sensitivity = 0.4;
    m_movement_speed = 5.0;
    m_height = 1.0;
    m_radius = 0.2;
    m_mass = 10.0;
}

pm3::PIXELPLAYER::~PIXELPLAYER() {
    unload();
}

void pm3::PIXELPLAYER::unload() {
    delete m_rigid_body;
    delete m_collision_shape;
}

void pm3::PIXELPLAYER::bind() {
    tolua_ogre_vector3_open( m_lua_state );
    tolua_playerscript_open( m_lua_state );
}

void pm3::PIXELPLAYER::setup_body() {
    pm3::PHYSICS *physics = pm3::PHYSICS::getSingletonPtr();
    LOGGER *log = LOGGER::getSingletonPtr();
    static bool done = false;

    if (done) {
        if (log)
            log->report( "Physics", "W: Player body must only be set up once\n" );
        return;
    }

    if (!physics || !physics->get_physics_world()) {
        if (log)
            log->report( "Physics", "E: Physics must be initialized before the post-setup of pixelplayer\n" );
        return;
    }

    if (!m_camera_node) {
        if (log)
            log->report( "Physics", "E: Player node must be initialized before the setup of player body\n" );
        return;
    }

    btVector3 inertia;

    //! \todo Add support for setting player radius, height and mass
    m_collision_shape = new btCapsuleShape( m_radius, m_height );
    m_collision_shape->calculateLocalInertia( m_mass, inertia );

    // Create a state
    m_state = new BtOgre::RigidBodyState( m_camera_node );

    // Create the body
    m_rigid_body = new btRigidBody( m_mass, m_state, m_collision_shape, inertia );

    // Disable sleeping
    m_rigid_body->setSleepingThresholds( 0, 0 );
    // Disable rotation
    m_rigid_body->setAngularFactor( 0 );
    // Disable contact processing to avoid bumpy behaviour on internal edges
    m_rigid_body->setContactProcessingThreshold( 0.005f );

    // Detach on god mode
/*    if (m_god_mode)
        m_state->setNode( NULL );*/

    // Add the body for physics action
    physics->get_physics_world()->addRigidBody( m_rigid_body );

    // Disable physics to avoid player dropping into -infinity before a map is loaded
    set_physics_active( false );

    done = true;
}

void pm3::PIXELPLAYER::init() {
    Ogre::PCZSceneManager *scene_mgr = pm3::INTERFACE::getSingleton().get_scenemgr();

    // Make sure we only create the nodes once
    // and load the configuration once
    if (!m_camera_node) {
        m_camera_node = (Ogre::PCZSceneNode *) scene_mgr->getRootSceneNode()->createChildSceneNode( "CameraNode" );
        m_camera_yaw_node = (Ogre::PCZSceneNode *) m_camera_node->createChildSceneNode( "CameraYawNode" );
        m_camera_pitch_node = (Ogre::PCZSceneNode *) m_camera_yaw_node->createChildSceneNode( "CameraPitchNode" );
        m_camera_roll_node = (Ogre::PCZSceneNode *) m_camera_pitch_node->createChildSceneNode( "CameraRollNode" );

        // Create camera
/*        m_camera = scene_mgr->createCamera( "PixelCamera" );
        m_camera->setNearClipDistance( 0.1f );
        m_camera->setFarClipDistance( 1000.0f );*/
        m_camera = pm3::CAMERA_FACTORY::getSingleton().create_camera( "PixelCamera" );

        // Attach camera
        m_camera_roll_node->attachObject( m_camera );

        // Load player configuration
        load( "player.lua" );

//        dump();

        //! \todo Attach an animated model to player
        setup_body();
    }
}

void pm3::PIXELPLAYER::set_zone( Ogre::PCZone *p_zone ) {
    if (m_camera_node)
        m_camera_node->setHomeZone( p_zone );
    if (m_camera_yaw_node)
        m_camera_yaw_node->setHomeZone( p_zone );
    if (m_camera_pitch_node)
        m_camera_pitch_node->setHomeZone( p_zone );
    if (m_camera_roll_node)
        m_camera_roll_node->setHomeZone( p_zone );
}

void pm3::PIXELPLAYER::set_god_mode( bool b_god ) {
    Ogre::PCZSceneManager *scene_mgr = pm3::INTERFACE::getSingleton().get_scenemgr();
    static Ogre::PCZone *zone = NULL;
    static Ogre::ViewPoint vp;

    m_god_mode = b_god;

    if (m_state) {
        if (m_god_mode) {
            // Store player viewpoint
            vp.position = m_camera_node->getPosition();
            vp.orientation = m_camera_node->getOrientation();

            // Detach player camera node from physics engine
            m_state->setNode( NULL );
            // Store player zone
            zone = m_camera_node->getHomeZone();
            // Set player home zone as default (all-encompassing zone)
            set_zone( scene_mgr->getDefaultZone() );
        } else {
            // Attach player camera node to physics engine
            m_state->setNode( m_camera_node );
            // Set player home zone back
            set_zone( zone );

            // Restore player viewpoint
            m_camera_node->setPosition( vp.position );
            m_camera_node->setOrientation( vp.orientation );
        }
    }
}

void pm3::PIXELPLAYER::angle_rotate( Ogre::Degree f_rot_x, Ogre::Degree f_rot_y ) {
    // Yaws the player by an angle around the Y world axis
    if (m_camera_yaw_node)
        m_camera_yaw_node->yaw( f_rot_x * m_mouse_sensitivity );
    // Pitches the player by an angle around the X local axis
    if (m_camera_pitch_node)
        m_camera_pitch_node->pitch( f_rot_y * m_mouse_sensitivity );
}

void pm3::PIXELPLAYER::move( const Ogre::Vector3 &r_t ) {
    Ogre::Degree pitch;
    Ogre::Vector3 vec;
    float pitch_sign = 0.0f;

    // Only move player body when not in god mode
    if (m_camera_yaw_node && m_camera_pitch_node && m_rigid_body && !m_god_mode) {
        vec = m_camera_yaw_node->getOrientation() * m_camera_pitch_node->getOrientation() * r_t * m_movement_speed;
        vec.y = m_rigid_body->getLinearVelocity().y();

        //! \todo Implement btCharacterControllerInterface instead of a rigid body for the player?

        // Move player
        m_rigid_body->setLinearVelocity( BtOgre::Convert::toBullet( vec ) );
    }

    if (m_camera_node && m_camera_pitch_node && m_camera_yaw_node) {
        // Move the camera node directly in god mode
        if (m_god_mode) {
            //! \bug Doesn't seem to be working anymore for some reason
            m_camera_node->translate( m_camera_yaw_node->getOrientation() *
                                      m_camera_pitch_node->getOrientation() *
                                      r_t * 3.0, Ogre::SceneNode::TS_LOCAL );
        }

        // Angle of rotation around the X-axis.
        pitch = ( 2 * Ogre::Degree( Ogre::Math::ACos( m_camera_pitch_node->getOrientation().w ) ).valueDegrees() );

        pitch_sign = m_camera_pitch_node->getOrientation().x;

        // Limit the pitch between -90 degress and +90 degrees
        if (pitch > Ogre::Degree( 90.0f ) ) {
            if (pitch_sign > 0) {
                // Set orientation to 90 degrees on X-axis.
                m_camera_pitch_node->setOrientation( Ogre::Quaternion( Ogre::Math::Sqrt( 0.5f ), Ogre::Math::Sqrt( 0.5f ), 0, 0 ) );
            } else if (pitch_sign < 0) {
                // Sets orientation to -90 degrees on X-axis.
                m_camera_pitch_node->setOrientation( Ogre::Quaternion( Ogre::Math::Sqrt( 0.5f ), -Ogre::Math::Sqrt( 0.5f ), 0, 0 ) );
            }
        }
    }
}

void pm3::PIXELPLAYER::stop() {
    Ogre::Vector3 vec;

    if (m_rigid_body && !m_god_mode) {
        // Dampen movement in X, Z axis and keep Y axis untouched
        vec.x = m_rigid_body->getLinearVelocity().x() * 0.5;
        vec.y = m_rigid_body->getLinearVelocity().y();
        vec.z = m_rigid_body->getLinearVelocity().z() * 0.5;

        // Reset player velocity
        m_rigid_body->setLinearVelocity( BtOgre::Convert::toBullet( vec ) );
    }
}

void pm3::PIXELPLAYER::teleport( const Ogre::Vector3 &r_pos, Ogre::PCZone *p_zone ) {
    pm3::PHYSICS *physics = pm3::PHYSICS::getSingletonPtr();
    LOGGER *log = LOGGER::getSingletonPtr();

    if (!physics || !physics->get_physics_world()) {
        if (log)
            log->report( "Physics", "E: Physics must be initialized before teleporting the player\n" );
        return;
    }

    if (!m_camera_node) {
        if (log)
            log->report( "Physics", "E: Player node must be initialized before teleporting the player\n" );
        return;
    }

    if (log)
        log->report( "Pixel", "I: Teleporting player into zone \"%s\"\n", p_zone->getName().c_str() );

    if (m_rigid_body) {
        btTransform transform;

        // Remove the body from physics
        physics->get_physics_world()->removeRigidBody( m_rigid_body );
        // Perform the teleportation
        m_state->getWorldTransform( transform );
        transform.setOrigin( BtOgre::Convert::toBullet( r_pos ) );
        m_state->setWorldTransform( transform );
        m_rigid_body->setMotionState( m_state );
        // And add it back
        physics->get_physics_world()->addRigidBody( m_rigid_body );
    } else {
        if (log)
            log->report( "Physics", "E: Player body must be initialized before teleporting the player\n" );
    }

    // Update player homezone
    set_zone( p_zone );
}

std::string pm3::PIXELPLAYER::get_pos_str() const {
    std::string result;
    Ogre::Vector3 pos, angles;

    if (!m_camera_node)
        return "";

    // Position
    pos = m_camera_node->getPosition();
    // Magnitudes
    angles.x = Ogre::Math::Floor( Ogre::Degree( m_camera_pitch_node->getOrientation().getPitch() ).valueDegrees() );
    angles.y = Ogre::Math::Floor( Ogre::Degree( m_camera_yaw_node->getOrientation().getYaw() ).valueDegrees() );
    angles.z = Ogre::Math::Floor( Ogre::Degree( m_camera_roll_node->getOrientation().getRoll() ).valueDegrees() );

    // Build the string
    result = "CAM: (" + Ogre::StringConverter::toString( pos.x ) + ", " + 
                        Ogre::StringConverter::toString( pos.y ) + ", " +
                        Ogre::StringConverter::toString( pos.z ) + ")(" +
                        Ogre::StringConverter::toString( angles.x ) + ", " + 
                        Ogre::StringConverter::toString( angles.y ) + ", " + 
                        Ogre::StringConverter::toString( angles.z ) + ")";

    if (get_zone())
        result += " \"" + get_zone()->getName() + "\"";

    if (get_god_mode())
        result += " GOD";

    return result;
}

void pm3::PIXELPLAYER::set_orientation( const Ogre::Quaternion &r_quat ) {
    // Reset all rotation around the X axis
    if (m_camera_pitch_node)
        m_camera_pitch_node->setOrientation( Ogre::Quaternion() );
    // Set rotation around the Y axis
    if (m_camera_yaw_node) {
        // Set to north by default
        m_camera_yaw_node->setOrientation( pm3::util::direction_to_quat( pm3::util::D_N ) );
        // And rotate from there
        m_camera_yaw_node->yaw( -Ogre::Degree( r_quat.getYaw() ) );
    }
}

Ogre::Vector3 pm3::PIXELPLAYER::get_grid_pos() const {
    Ogre::Vector3 pos;

    if (pm3::lua::g_active_map)
        pos = pm3::util::round( m_camera_node->getPosition() / pm3::lua::g_active_map->get_room_size() );

    return pos;
}

Ogre::Vector3 pm3::PIXELPLAYER::get_world_pos() const {
    return m_camera_node->getPosition();
}

Ogre::PCZone *pm3::PIXELPLAYER::get_zone() const {
    if (m_camera_node)
        return m_camera_node->getHomeZone();
    return NULL;
}

int pm3::PIXELPLAYER::teleport( int i_layer, int i_x, int i_y ) {
    LOGGER *log = LOGGER::getSingletonPtr();
    Ogre::ViewPoint viewpoint;
    PIXELOBJ *obj = NULL;
    PIXELROOM *room = NULL;
    int direction = 0;

    // Make sure there's a a map active
    if (!pm3::lua::g_active_map) {
        if (log)
            log->report( "Lua", "E: Map not active\n" );
        return -1;
    }

    // Check that we don't teleport player somewhere out the maze
    obj = pm3::lua::g_active_map->get_obj( i_layer, i_x, i_y );

    if (!obj)
        return -1;

    // If there's an object, there must be a room also
    room = pm3::lua::g_active_map->get_room( i_layer, i_x, i_y );

    // Convert the grid position into world position
    viewpoint.position = pm3::util::round( Ogre::Vector3( i_x, i_layer, i_y ) * pm3::lua::g_active_map->get_room_size() );
    // Teleport player
    teleport( viewpoint.position, room->get_zone() );
    // Find a random direction (of all possibilities) that the player should point to
    direction = pm3::util::get_rand_direction( obj->get_directions() );
    if (direction != pm3::util::D_NONE) {
        // Calculate a quaternion based on the direction
        viewpoint.orientation = pm3::util::direction_to_quat( direction );
        // Set player orientation
        set_orientation( viewpoint.orientation );
    }

    // There are no results
    return 0;
}

void pm3::PIXELPLAYER::dump() const {
    LOGGER *log = LOGGER::getSingletonPtr();

    if (log) {
        log->report( "Pixel", "I: Player dump:\n" );
        log->report( "Pixel", "   Mouse sensitivity = %f\n", m_mouse_sensitivity );
        log->report( "Pixel", "   Movement speed = %f\n\n", m_movement_speed );
        log->report( "Pixel", "   Player collision height = %f\n", m_height );
        log->report( "Pixel", "   Player collision radius = %f\n", m_radius );
        log->report( "Pixel", "   Player mass = %f\n", m_mass );
    }
}

std::string pm3::PIXELPLAYER::save_script( const std::string &r_path ) {
    if (r_path.empty())
        return "";

    FILE *fo = fopen( r_path.c_str(), "wt" );

    if (fo) {
        fprintf( fo, "-- Player configuration\n" );
        fprintf( fo, "player = get_player();\n\n" );

        fprintf( fo, "player:set_mouse_sensitivity( %f );\n", m_mouse_sensitivity );
        fprintf( fo, "player:set_movement_speed( %f );\n", m_movement_speed );
        fprintf( fo, "player:set_player_dimensions( %f, %f );\n", m_radius, m_height );
        fprintf( fo, "player:set_player_mass( %f );\n", m_mass );

        // Mark the player script as saved
        m_unsaved = false;

        fclose( fo );

        return r_path;
    }

    return "";
}

void pm3::PIXELPLAYER::set_physics_active( bool b_active ) {
    if (!m_rigid_body)
        return;

    m_physics_active = b_active;

    if (b_active) {
        m_rigid_body->activate();
    } else {
        m_rigid_body->setActivationState( 0 );
    }
}

